Search K
Appearance
Appearance
从大学开始懵懵懂懂粗略学习(死记硬背)了一些 TCP 协议的内容,到工作多年以后,一直没有找到顺手的网络协议栈调试工具,对于纷繁复杂 TCP 协议。业界流行的 scapy 不是很好用,有很多局限性。直到前段时间看到了 Google 开源的 packetdrill,真有一种相见恨晚的感觉。这篇文章讲介绍 packetdrill 的基本原理和用法。

packetdrill 在 2013 年开源,在 Google 内部久经考验,Google 用它发现了 10 余个 Linux 内核 bug,同时用测试驱动开发的方式开发新的网络特性和进行回归测试,确保新功能的添加不影响网络协议栈的可用性。
以 centos7 为例
cd gtests/net/packetdrillsudo yum install -y bison flex./configureMakefile,去掉第一行的末尾的 -staticpacketdrill 脚本采用 c 语言和 tcpdump 混合的语法。脚本文件名一般以 .pkt 为后缀,执行脚本的方式为 sudo ./packetdrill test.pkt
脚本的每一行可以由以下几种类型的语句构成:
脚本每一行都有一个时间参数用来表明执行的时间或者预期事件发生的时间,packetdrill 支持绝对时间和相对时间。绝对时间就是一个简单的数字,相对时间会在数字前面添加一个 + 号。比如下面这两个例子
// 300ms 时执行 accept 调用
0.300 accept(3, ..., ...) = 4
// 在上一行语句执行结束 10ms 以后执行
+.010 write(4, ..., 1000) = 1000`如果预期的事件在指定的时间没有发生,脚本执行会抛出异常,由于不同机器的响应时间不同,所以 packetdrill 提供了参数(--tolerance_usecs)用来设置误差范围,默认值是 4000us(微秒),也即 4ms。这个参数默认值在 config.c 的 set_default_config 函数里进行设置 config->tolerance_usecs = 4000;
我们以一个最简单的 demo 来演示 packetdrill 的用法。乍一看很懵,容我慢慢道来
1 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
2 +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
3 +0 bind(3, ..., ...) = 0
4 +0 listen(3, 1) = 0
5
6 //TCP three-way handshake
7 +0 < S 0:0(0) win 4000 <mss 1000>
8 +0 > S. 0:0(0) ack 1 <...>
9 +.1 < . 1:1(0) ack 1 win 1000
10
11 +0 accept(3, ..., ...) = 4
12 +0 write(4, ..., 10) = 10
13 +0 > P. 1:11(10) ack 1
14 +.1 < . 1:1(0) ack 6 win 1000第 1 行:0 socket(…, SOCK_STREAM, IPPROTO_TCP) = 3
在脚本执行的第 0s 创建一个 socket,使用的是系统调用的方式,socket 函数的签名和用法如下
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
成功时返回文件描述符,失败时返回 -1
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);在 packetdrill 脚本中用 … 来表示当前参数省略不相关的细节信息,使用 packetdrill 程序的默认值。
脚本返回新建的 socket 文件句柄,这里用 = 来断言会返回 3,因为 linux 在每个程序开始的时刻,都会有 3 个已经打开的文件句柄,分别是:标准输入 stdin(0)、标准输出 stdout(1)、错误输出 stderr(2) 默认的,其它新建的文件句柄则排在之后,从 3 开始。

2 +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
3 +0 bind(3, ..., ...) = 0
4 +0 listen(3, 1) = 0第 7 ~ 9 行是经典的三次握手,packetdrill 的语法非常类似 tcpdump 的语法
< 表示输入的数据包(input packets),packetdrill 会构造一个真实的数据包,注入到内核协议栈。比如:
// 构造 SYN 包注入到协议栈
+0 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
// 构造 icmp echo_reply 包注入到协议栈
0.400 < icmp echo_reply> 表示预期协议栈会响应的包(outbound packets),这个包不是 packetdrill 构造的,是由协议栈发出的,packetdrill 会检查协议栈是不是真的发出了这个包,如果没有,则脚本报错停止执行。比如
// 调用 write 函数调用以后,检查协议栈是否真正发出了 PSH+ACK 包
+0 write(4, ..., 1000) = 1000
+0 > P. 1:1001(1000) ack 1
// 三次握手中过程向协议栈注入 SYN 包以后,检查协议栈是否发出了 SYN+ACK 包以及 ack 是否等于 1
0.100 < S 0:0(0) win 32792 <mss 1000,nop,wscale 7>
0.100 > S. 0:0(0) ack 1 <mss 1460,nop,wscale 6>
第 7 行:+0 < S 0:0(0) win 1000 <mss 1000>
packetdrill 构造一个 SYN 包发送到协议栈,它使用与 tcpdump 类似的相对 sequence 序号,S 后面的三个 0,分别表示发送包的起始 seq、结束 seq、包的长度。比如 P. 1:1001(1000) 表示发送的包起始序号为 1,结束 seq 为 1001,长度为 1000。紧随其后的 win 表示发送端的接收窗口大小 1000。依据 TCP 协议,SYN 包也必须带上自身的 MSS 选项,这里的 MSS 大小为 1000
第 8 行:+0 > S. 0:0(0) ack 1 <…>
预期协议栈会立刻回复 SYN+ACK 包,因为还没有发送数据,所以包的 seq 开始值、结束值、长度都为 0,ack 为上次 seq + 1,表示第一个 SYN 包已收到。
第 9 行:+.1 < . 1:1(0) ack 1 win 1000
0.1s 以后注入一个 ACK 包到协议栈,没有携带数据,包的长度为 0,至此三次握手完成,过程如下图

+0 accept(3, …, …) = 4 accept 系统调用返回了一个值为 4 的新的文件 fd,这时 packetdrill 可以往这个 fd 里面写数据了
+0 write(4, ..., 10)=10
+0 > P. 1:11(10) ack 1
+.1 < . 1:1(0) ack 11 win 1000packetdrill 调用 write 函数往 socket 里写了 10 字节的数据,协议栈立刻发出这 10 个字节数据包,同时把 PSH 标记置为 1。这个包的起始 seq 为 1,结束 seq 为 10,长度为 10。100ms 以后注入 ACK 包,模拟协议栈收到 ACK 包。
整个过程如下 
采用 tcpdump 对 8080 端口进行抓包,结果如下
sudo tcpdump -i any port 8080 -nn
10:02:36.591911 IP 192.0.2.1.37786 > 192.168.31.139.8080: Flags [S], seq 0, win 4000, options [mss 1000], length 0
10:02:36.591961 IP 192.168.31.139.8080 > 192.0.2.1.37786: Flags [S.], seq 2327356581, ack 1, win 29200, options [mss 1460], length 0
10:02:36.693785 IP 192.0.2.1.37786 > 192.168.31.139.8080: Flags [.], ack 1, win 1000, length 0
10:02:36.693926 IP 192.168.31.139.8080 > 192.0.2.1.37786: Flags [P.], seq 1:11, ack 1, win 29200, length 10
10:02:36.801092 IP 192.0.2.1.37786 > 192.168.31.139.8080: Flags [.], ack 11, win 1000, length 0在脚本的最后一行,加上
+0 `sleep 1000000`让脚本执行完不要退出,执行 ifconfig 可以看到,比没有执行脚本之前多了一个虚拟的网卡 tun0。

packetdrill 就是在执行脚本前创建了一个名为 tun0 的虚拟网卡,脚本执行完,tun0 会被销毁。该虚拟网卡对应于操作系统中 /dev/net/tun 文件,每次程序通过 write 等系统调用将数据写入到这个文件 fd 时,这些数据会经过 tun0 这个虚拟网卡,将数据写入到内核协议栈,read 系统调用读取数据的过程类似。协议栈可以向操作普通网卡一样操作虚拟网卡 tun0。
关于 linux 下 tun 的详细使用介绍,可以参考 IBM 的文章 www.ibm.com/developerwo…
把 packetdrill 加入到环境变量里以便于可以在任意目录可以执行。第一步是修改 /etc/profile 或者 .zshrc(如果你用的是最好用的 zsh 的话)等可以修改环境变量的文件。
export PATH=/path_to_packetdrill/:$PATH
source ~/.zshrc在命令行中输入 packetdrill 如果有输出 packetdrill 的 usage 文档说明第一步成功啦。
但是 packetdrill 命令是需要 sudo 权限执行的,如果现在我们在命令行中输入 sudo packetdrill,会提示找不到 packetdrill 命令
sudo:packetdrill:找不到命令这是因为 sudo 命令为了安全性的考虑,覆盖了用户自己 PATH 环境变量,我们可以用 sudo sudo -V | grep PATH 来看
sudo sudo -V | grep PATH
# 覆盖用户的 $PATH 变量的值:/sbin:/bin:/usr/sbin:/usr/bin可以看到 sudo 命令覆盖了用户的 PATH 变量。这些初始值是在 /etc/sudoers 中定义的
sudo cat /etc/sudoers | grep -i PATH
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin一个最简单的办法是在 sudo 启动时重新赋值它的 PATH 变量:sudo env PATH="$PATH" cmd_x,可以用 sudo env PATH="$PATH" env | grep PATH 与 sudo env | grep PATH 做前后对比

对于本文中的 packetdrill,可以用 sudo env PATH=$PATH packetdrill delay_ack.pkt 来执行,当然你可以做一个 sudo 的 alias
alias sudo='sudo env PATH="$PATH"'这样就可以在任意地方执行 sudo packetdrill 了
packetdrill 上手的难度有一点大,但是熟悉了以后用起来特别顺手,后面很多 TCP 包超时重传、快速重传、滑动窗口、nagle 算法都是会用这个工具来进行测试,希望你可以熟练掌握。